import fsExtra = require('fs-extra');
import path = require('path');
import { Gltf } from './gltfType';
import {
    FLOAT32_SIZE_BYTES,
    UINT16_SIZE_BYTES,
    UINT8_SIZE_BYTES,
    UINT32_SIZE_BYTES
} from './typeSize';
import {
    Matrix4,
    Cartesian3,
    Math as CesiumMath,
    defaultValue
} from 'cesium';

const createI3dm = require('./createI3dm');

export interface InstanceTileOptions {
    uri: string;
    tileWidth?: number;
    transform?: Matrix4;
    instancesLength?: number;
    embed?: boolean;
    modelSize?: number;
    createBatchTable?: boolean;
    createBatchTableBinary?: boolean;
    relativeToCenter?: boolean;
    quantizePositions?: boolean;
    eastNorthUp?: boolean;
    orientations?: boolean;
    octEncodeOrientations?: boolean;
    uniformScales?: boolean;
    nonUniformScales?: boolean;
    batchIds?: boolean;
    use3dTilesNext?: boolean;
    useGlb?: boolean;
}

export interface InstancesTileResult {
    glb?: Buffer,
    gltf?: Gltf,
    i3dm?: any,
    batchTableJson?: any
}

export async function createSampleInstancesTile(
    options: InstanceTileOptions
): Promise<InstancesTileResult> {
    // Set the random number seed before creating the instances so that the generated instances are the same between runs
    CesiumMath.setRandomNumberSeed(0);

    options = defaultValue(options, {});
    const transform = defaultValue(options.transform, Matrix4.IDENTITY);
    const instancesLength = defaultValue(options.instancesLength, 25);
    let uri = options.uri;
    const embed = defaultValue(options.embed, true) as boolean;
    const modelSize = defaultValue(options.modelSize, 20.0) as number;
    const createBatchTable = defaultValue(options.createBatchTable, true);
    const createBatchTableBinary = defaultValue(
        options.createBatchTableBinary,
        false
    );
    const batchIds = defaultValue(options.batchIds, false);

    const featureTableJson: any = {};
    featureTableJson.INSTANCES_LENGTH = instancesLength;

    let attributes = [];

    attributes.push(
        getSamplePosition(transform, options)
    );

    // attributes = attributes.concat(getSampleOrientations(25));
    featureTableJson.EAST_NORTH_UP = true;

    attributes.push(getSampleNonUniformScales(25, options));

    if (batchIds) {
        attributes.push(getBatchIds(instancesLength));
    }

    let i;
    let attribute;
    let byteOffset = 0;
    let attributesLength = attributes.length;
    for (i = 0; i < attributesLength; ++i) {
        attribute = attributes[i];
        const byteAlignment = attribute.byteAlignment;
        byteOffset = Math.ceil(byteOffset / byteAlignment) * byteAlignment; // Round up to the required alignment
        attribute.byteOffset = byteOffset;
        byteOffset += attribute.buffer.length;
    }

    const featureTableBinary = Buffer.alloc(byteOffset);

    for (i = 0; i < attributesLength; ++i) {
        attribute = attributes[i];
        featureTableJson[attribute.propertyName] = {
            byteOffset: attribute.byteOffset,
            componentType: attribute.componentType // Only defined for batchIds
        };
        attribute.buffer.copy(featureTableBinary, attribute.byteOffset);
    }

    let batchTableJson;
    let batchTableBinary;
    if (createBatchTable) {
        if (createBatchTableBinary) {
            const batchTable = generateBatchTableBinary(instancesLength);
            batchTableJson = batchTable.json;
            batchTableBinary = batchTable.binary;
        } else {
            batchTableJson = generateInstancesBatchTable(
                instancesLength,
                modelSize
            );
        }
    }

    let glb = await fsExtra.readFile(uri);
    glb = embed ? glb : undefined;
    uri = path.basename(uri);
    const i3dm = createI3dm({
        featureTableJson: featureTableJson,
        featureTableBinary: featureTableBinary,
        batchTableJson: batchTableJson,
        batchTableBinary: batchTableBinary,
        glb: glb,
        uri: uri
    });
    return {
        i3dm: i3dm,
        batchTableJson: batchTableJson
    };
}

function getSamplePosition(transform, option){
    // 环和柱各需 25个点位
    var instancesLength = 25;
    // 单位长度
    const buffer = Buffer.alloc(instancesLength * 3 * FLOAT32_SIZE_BYTES);
    for (let i = 0; i < instancesLength; ++i) {
        let position;

        if (option.tilesetName == 'CylinderInstanced')
            switch (i + 1) {
                case 1: case 5:
                    position = new Cartesian3(0.0, 0.0, 0.0); break;
                case 2: case 6:
                    position = new Cartesian3(5.0, 0.0, 0.0); break;
                case 3: case 7:
                    position = new Cartesian3(5.0, 5.0, 0.0); break;
                case 4: case 8:
                    position = new Cartesian3(0.0, 5.0, 0.0); break;
                case 9: position = new Cartesian3(0.0, 0.0, 5.0); break;
                case 10: position = new Cartesian3(5.0, 0.0, 5.0); break;
                case 11: position = new Cartesian3(5.0, 5.0, 5.0); break;
                case 12: position = new Cartesian3(0.0, 5.0, 5.0); break;

                case 13: case 17:
                    position = new Cartesian3(10.0, 0.0, 0.0); break;
                case 14: case 18:
                    position = new Cartesian3(30.0, 0.0, 0.0); break;
                case 15: case 19:
                    position = new Cartesian3(30.0, 20.0, 0.0); break;
                case 16: case 20:
                    position = new Cartesian3(10.0, 20.0, 0.0); break;
                case 21: position = new Cartesian3(10.0, 0.0, 20.0); break;
                case 22: position = new Cartesian3(30.0, 0.0, 20.0); break;
                case 23: position = new Cartesian3(30.0, 20.0, 20.0); break;
                case 24: position = new Cartesian3(10.0, 20.0, 20.0); break;

                case 25: position = new Cartesian3(-5.0, 0.0, 0.0); break;

                default:
                    break;
            }
        else if (option.tilesetName == 'TorusInstanced')
            switch (i + 1) {
                case 1: position = new Cartesian3(2.5, 0.0, 0.0); break;
                case 2: position = new Cartesian3(5.0, 2.5, 0.0); break;
                case 3: position = new Cartesian3(2.5, 2.5, 0.0); break;
                case 4: position = new Cartesian3(0.0, 2.5, 0.0); break;
                case 5: position = new Cartesian3(0.0, 0.0, 2.5); break;
                case 6: position = new Cartesian3(5.0, 0.0, 2.5); break;
                case 7: position = new Cartesian3(5.0, 5.0, 2.5); break;
                case 8: position = new Cartesian3(0.0, 5.0, 2.5); break;
                case 9: position = new Cartesian3(2.5, 0.0, 5.0); break;
                case 10: position = new Cartesian3(5.0, 2.5, 5.0); break;
                case 11: position = new Cartesian3(2.5, 2.5, 5.0); break;
                case 12: position = new Cartesian3(0.0, 2.5, 5.0); break;

                case 13: position = new Cartesian3(20.0, 0.0, 0.0); break;
                case 14: position = new Cartesian3(30.0, 10.0, 0.0); break;
                case 15: position = new Cartesian3(20.0, 20.0, 0.0); break;
                case 16: position = new Cartesian3(10.0, 10.0, 0.0); break;
                case 17: position = new Cartesian3(10.0, 0.0, 10.0); break;
                case 18: position = new Cartesian3(30.0, 0.0, 10.0); break;
                case 19: position = new Cartesian3(30.0, 20.0, 10.0); break;
                case 20: position = new Cartesian3(10.0, 20.0, 10.0); break;
                case 21: position = new Cartesian3(20.0, 0.0, 20.0); break;
                case 22: position = new Cartesian3(30.0, 10.0, 20.0); break;
                case 23: position = new Cartesian3(20.0, 20.0, 20.0); break;
                case 24: position = new Cartesian3(10.0, 10.0, 20.0); break;

                case 25: position = new Cartesian3(-5.0, 0.0, 0.0); break;
            }

        Matrix4.multiplyByPoint(transform, position, position);

        buffer.writeFloatLE(position.x, i * 3 * FLOAT32_SIZE_BYTES);
        buffer.writeFloatLE(position.y, (i * 3 + 1) * FLOAT32_SIZE_BYTES);
        buffer.writeFloatLE(position.z, (i * 3 + 2) * FLOAT32_SIZE_BYTES);
    }
    return {
        buffer: buffer,
        propertyName: 'POSITION',
        byteAlignment: FLOAT32_SIZE_BYTES
    };
}

function getSampleNonUniformScales(instancesLength, options) {
    const buffer = Buffer.alloc(instancesLength * 3 * FLOAT32_SIZE_BYTES);
    if (options.tilesetName == 'CylinderInstanced')
        for (let i = 0; i < instancesLength; ++i) {
            buffer.writeFloatLE(i < 12 ? 5 : i < 25 ? 20 : 1, i * 3 * FLOAT32_SIZE_BYTES);
            buffer.writeFloatLE(1, (i * 3 + 1) * FLOAT32_SIZE_BYTES);
            buffer.writeFloatLE(1, (i * 3 + 2) * FLOAT32_SIZE_BYTES);
        }
    else if (options.tilesetName == 'TorusInstanced')
        for (let i = 0; i < instancesLength; ++i) {
            buffer.writeFloatLE(1, i * 3 * FLOAT32_SIZE_BYTES);
            buffer.writeFloatLE(12 <= i && i < 24 ? 2 : 1, (i * 3 + 1) * FLOAT32_SIZE_BYTES);
            buffer.writeFloatLE(12 <= i && i < 24 ? 2 : 1, (i * 3 + 2) * FLOAT32_SIZE_BYTES);
        }
    return {
        buffer: buffer,
        propertyName: 'SCALE_NON_UNIFORM',
        byteAlignment: FLOAT32_SIZE_BYTES
    };
}

function getBatchIds(instancesLength: number) {
    let i: number;
    let buffer: Buffer;
    let componentType: string;
    let byteAlignment: number;

    if (instancesLength < 256) {
        buffer = Buffer.alloc(instancesLength * UINT8_SIZE_BYTES);
        for (i = 0; i < instancesLength; ++i) {
            buffer.writeUInt8(i, i * UINT8_SIZE_BYTES);
        }
        componentType = 'UNSIGNED_BYTE';
        byteAlignment = UINT8_SIZE_BYTES;
    } else if (instancesLength < 65536) {
        buffer = Buffer.alloc(instancesLength * UINT16_SIZE_BYTES);
        for (i = 0; i < instancesLength; ++i) {
            buffer.writeUInt16LE(i, i * UINT16_SIZE_BYTES);
        }
        componentType = 'UNSIGNED_SHORT';
        byteAlignment = UINT16_SIZE_BYTES;
    } else {
        buffer = Buffer.alloc(instancesLength * UINT32_SIZE_BYTES);
        for (i = 0; i < instancesLength; ++i) {
            buffer.writeUInt32LE(i, i * UINT32_SIZE_BYTES);
        }
        componentType = 'UNSIGNED_INT';
        byteAlignment = UINT32_SIZE_BYTES;
    }

    return {
        buffer: buffer,
        componentType: componentType,
        propertyName: 'BATCH_ID',
        byteAlignment: byteAlignment
    };
}

export function generateInstancesBatchTable(
    instancesLength: number,
    modelSize: number
) {
    return {
        Height: new Array(instancesLength).fill(modelSize)
    };
}

function generateBatchTableBinary(instancesLength: number) {
    const idBuffer = Buffer.alloc(instancesLength * UINT32_SIZE_BYTES);
    for (let i = 0; i < instancesLength; ++i) {
        idBuffer.writeUInt32LE(i, i * UINT32_SIZE_BYTES);
    }
    const batchTableJson = {
        id: { byteOffset: 0, componentType: 'UNSIGNED_INT', type: 'SCALAR' }
    };
    return { json: batchTableJson, binary: idBuffer };
}
